page-data-props.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import type { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
  2. import type {
  3. IDataWithRequiredMeta,
  4. IPage,
  5. IPageInfoBasic,
  6. IPageNotFoundInfo,
  7. IUser,
  8. } from '@growi/core';
  9. import { isIPageInfo, isIPageNotFoundInfo } from '@growi/core';
  10. import {
  11. isPermalink as _isPermalink,
  12. isTopPage,
  13. isUserPage,
  14. isUsersTopPage,
  15. } from '@growi/core/dist/utils/page-path-utils';
  16. import { removeHeadingSlash } from '@growi/core/dist/utils/path-utils';
  17. import assert from 'assert';
  18. import type { HydratedDocument, model } from 'mongoose';
  19. import type { CrowiRequest } from '~/interfaces/crowi-request';
  20. import type { PageDocument, PageModel } from '~/server/models/page';
  21. import type {
  22. IPageRedirect,
  23. PageRedirectModel,
  24. } from '~/server/models/page-redirect';
  25. import { findPageAndMetaDataByViewer } from '~/server/service/page/find-page-and-meta-data-by-viewer';
  26. import type { CommonEachProps } from '../common-props';
  27. import type {
  28. GeneralPageInitialProps,
  29. IPageToShowRevisionWithMeta,
  30. } from '../general-page';
  31. import type { EachProps } from './types';
  32. // Utility to resolve path, redirect, and identical path page check
  33. type PathResolutionResult = {
  34. resolvedPagePath: string;
  35. isIdenticalPathPage: boolean;
  36. redirectFrom?: string;
  37. };
  38. let mongooseModel: typeof model;
  39. let Page: PageModel;
  40. let PageRedirect: PageRedirectModel;
  41. async function initModels(): Promise<void> {
  42. if (mongooseModel == null) {
  43. mongooseModel = (await import('mongoose')).model;
  44. }
  45. if (Page == null) {
  46. Page = mongooseModel<IPage, PageModel>('Page');
  47. }
  48. if (PageRedirect == null) {
  49. PageRedirect = mongooseModel<IPageRedirect, PageRedirectModel>(
  50. 'PageRedirect',
  51. );
  52. }
  53. }
  54. async function resolvePathAndCheckIdentical(
  55. path: string,
  56. user: IUser | undefined,
  57. ): Promise<PathResolutionResult> {
  58. await initModels();
  59. const isPermalink = _isPermalink(path);
  60. let resolvedPagePath = path;
  61. let redirectFrom: string | undefined;
  62. let isIdenticalPathPage = false;
  63. if (!isPermalink) {
  64. const chains = await PageRedirect.retrievePageRedirectEndpoints(path);
  65. if (chains != null) {
  66. resolvedPagePath = chains.end.toPath;
  67. redirectFrom = chains.start.fromPath;
  68. }
  69. const multiplePagesCount = await Page.countByPathAndViewer(
  70. resolvedPagePath,
  71. user,
  72. null,
  73. true,
  74. );
  75. isIdenticalPathPage = multiplePagesCount > 1;
  76. }
  77. return { resolvedPagePath, isIdenticalPathPage, redirectFrom };
  78. }
  79. /**
  80. * Convert pathname based on page data and permalink status
  81. * @returns Final pathname to be used in the URL
  82. */
  83. function resolveFinalizedPathname(
  84. pagePath: string,
  85. page: HydratedDocument<IPage> | null | undefined,
  86. isPermalink: boolean,
  87. ): string {
  88. let finalPathname = pagePath;
  89. if (page != null) {
  90. // /62a88db47fed8b2d94f30000 ==> /path/to/page
  91. if (isPermalink && page.isEmpty) {
  92. finalPathname = page.path;
  93. }
  94. // /path/to/page ==> /62a88db47fed8b2d94f30000
  95. if (!isPermalink && !page.isEmpty) {
  96. const isToppage = isTopPage(pagePath);
  97. if (!isToppage && page._id) {
  98. finalPathname = `/${page._id.toString()}`;
  99. }
  100. }
  101. }
  102. return finalPathname;
  103. }
  104. // Page data retrieval for initial load - returns GetServerSidePropsResult
  105. export async function getPageDataForInitial(
  106. context: GetServerSidePropsContext,
  107. ): Promise<
  108. GetServerSidePropsResult<
  109. Pick<GeneralPageInitialProps, 'pageWithMeta' | 'skipSSR'> &
  110. Pick<
  111. EachProps,
  112. 'currentPathname' | 'isIdenticalPathPage' | 'redirectFrom'
  113. >
  114. >
  115. > {
  116. const req: CrowiRequest = context.req as CrowiRequest;
  117. const { crowi, user } = req;
  118. const { revisionId } = req.query;
  119. // Parse path from URL
  120. let { path: pathFromQuery } = context.query;
  121. pathFromQuery = pathFromQuery != null ? (pathFromQuery as string[]) : [];
  122. let pathFromUrl = `/${pathFromQuery.join('/')}`;
  123. pathFromUrl = pathFromUrl === '//' ? '/' : pathFromUrl;
  124. const { pageService, pageGrantService, configManager } = crowi;
  125. const pageId = _isPermalink(pathFromUrl)
  126. ? removeHeadingSlash(pathFromUrl)
  127. : null;
  128. const isPermalink = _isPermalink(pathFromUrl);
  129. const { resolvedPagePath, isIdenticalPathPage, redirectFrom } =
  130. await resolvePathAndCheckIdentical(pathFromUrl, user);
  131. if (isIdenticalPathPage) {
  132. return {
  133. props: {
  134. currentPathname: resolvedPagePath,
  135. isIdenticalPathPage: true,
  136. pageWithMeta: null,
  137. skipSSR: false,
  138. redirectFrom,
  139. },
  140. };
  141. }
  142. // Get full page data
  143. const pageWithMeta = await findPageAndMetaDataByViewer(
  144. pageService,
  145. pageGrantService,
  146. { pageId, path: resolvedPagePath, user },
  147. );
  148. const isHidingUserPages = configManager.getConfig(
  149. 'security:isHidingUserPages',
  150. );
  151. if (isHidingUserPages && pageWithMeta.data != null) {
  152. const pagePath = pageWithMeta.data.path;
  153. const isTargetUserPage = isUserPage(pagePath) || isUsersTopPage(pagePath);
  154. if (isTargetUserPage) {
  155. return {
  156. props: {
  157. currentPathname: resolvedPagePath,
  158. isIdenticalPathPage: false,
  159. pageWithMeta: {
  160. data: null,
  161. meta: {
  162. isNotFound: true,
  163. isForbidden: true,
  164. },
  165. } satisfies IDataWithRequiredMeta<null, IPageNotFoundInfo>,
  166. skipSSR: false,
  167. redirectFrom,
  168. },
  169. };
  170. }
  171. }
  172. // Handle URL conversion
  173. const currentPathname = resolveFinalizedPathname(
  174. resolvedPagePath,
  175. pageWithMeta.data,
  176. isPermalink,
  177. );
  178. // When the page exists
  179. if (pageWithMeta.data != null) {
  180. const { data: page, meta } = pageWithMeta;
  181. // type assertion
  182. assert(isIPageInfo(meta), 'meta should be IPageInfo when data is not null');
  183. // Handle empty pages - return as not found to avoid serialization issues
  184. if (page.isEmpty) {
  185. return {
  186. props: {
  187. currentPathname,
  188. isIdenticalPathPage: false,
  189. pageWithMeta: {
  190. data: null,
  191. meta: {
  192. isNotFound: true,
  193. isForbidden: false,
  194. },
  195. } satisfies IDataWithRequiredMeta<null, IPageNotFoundInfo>,
  196. skipSSR: false,
  197. redirectFrom,
  198. },
  199. };
  200. }
  201. // Handle existing page with valid meta that is not IPageNotFoundInfo
  202. page.initLatestRevisionField(revisionId);
  203. const ssrMaxRevisionBodyLength = configManager.getConfig(
  204. 'app:ssrMaxRevisionBodyLength',
  205. );
  206. // Check if SSR should be skipped
  207. const latestRevisionBodyLength = await page.getLatestRevisionBodyLength();
  208. const skipSSR =
  209. latestRevisionBodyLength != null &&
  210. ssrMaxRevisionBodyLength < latestRevisionBodyLength;
  211. const populatedPage = await page.populateDataToShowRevision(skipSSR);
  212. return {
  213. props: {
  214. currentPathname,
  215. isIdenticalPathPage: false,
  216. pageWithMeta: {
  217. data: populatedPage,
  218. meta,
  219. } satisfies IPageToShowRevisionWithMeta,
  220. skipSSR,
  221. redirectFrom,
  222. },
  223. };
  224. }
  225. // type assertion
  226. assert(
  227. isIPageNotFoundInfo(pageWithMeta.meta),
  228. 'meta should be IPageNotFoundInfo when data is null',
  229. );
  230. // Handle the case where the page does not exist
  231. return {
  232. props: {
  233. currentPathname: resolvedPagePath,
  234. isIdenticalPathPage: false,
  235. pageWithMeta: pageWithMeta satisfies IDataWithRequiredMeta<
  236. null,
  237. IPageNotFoundInfo
  238. >,
  239. skipSSR: false,
  240. redirectFrom,
  241. },
  242. };
  243. }
  244. // Page data retrieval for same-route navigation
  245. export async function getPageDataForSameRoute(
  246. context: GetServerSidePropsContext,
  247. ): Promise<{
  248. props: Pick<CommonEachProps, 'currentPathname'> &
  249. Pick<EachProps, 'currentPathname' | 'isIdenticalPathPage' | 'redirectFrom'>;
  250. internalProps?: {
  251. pageWithMeta?:
  252. | IDataWithRequiredMeta<PageDocument, IPageInfoBasic>
  253. | IDataWithRequiredMeta<null, IPageNotFoundInfo>;
  254. };
  255. }> {
  256. const req: CrowiRequest = context.req as CrowiRequest;
  257. const { crowi, user } = req;
  258. const { pageService, pageGrantService } = crowi;
  259. const pathname = decodeURIComponent(
  260. context.resolvedUrl?.split('?')[0] ?? '/',
  261. );
  262. const pageId = _isPermalink(pathname) ? removeHeadingSlash(pathname) : null;
  263. const isPermalink = _isPermalink(pathname);
  264. const { resolvedPagePath, isIdenticalPathPage, redirectFrom } =
  265. await resolvePathAndCheckIdentical(pathname, user);
  266. if (isIdenticalPathPage) {
  267. return {
  268. props: {
  269. currentPathname: resolvedPagePath,
  270. isIdenticalPathPage: true,
  271. redirectFrom,
  272. },
  273. };
  274. }
  275. // For same route access, do minimal page lookup
  276. const pageWithMetaBasicOnly = await findPageAndMetaDataByViewer(
  277. pageService,
  278. pageGrantService,
  279. { pageId, path: resolvedPagePath, user, basicOnly: true },
  280. );
  281. const currentPathname = resolveFinalizedPathname(
  282. resolvedPagePath,
  283. pageWithMetaBasicOnly.data,
  284. isPermalink,
  285. );
  286. return {
  287. props: {
  288. currentPathname,
  289. isIdenticalPathPage: false,
  290. redirectFrom,
  291. },
  292. internalProps: {
  293. pageWithMeta: pageWithMetaBasicOnly.data?.isEmpty
  294. ? {
  295. data: null,
  296. meta: { isNotFound: true, isForbidden: false },
  297. }
  298. : pageWithMetaBasicOnly,
  299. },
  300. };
  301. }